Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tracking issue for illegal_floating_point_literal_pattern compatibility lint #41620

Closed
1 of 3 tasks
est31 opened this issue Apr 29, 2017 · 47 comments · Fixed by #116284
Closed
1 of 3 tasks

Tracking issue for illegal_floating_point_literal_pattern compatibility lint #41620

est31 opened this issue Apr 29, 2017 · 47 comments · Fixed by #116284
Labels
A-floating-point Area: Floating point numbers and arithmetic A-patterns Relating to patterns and pattern matching B-unstable Blocker: Implemented in the nightly compiler and unstable. C-future-compatibility Category: Future-compatibility lints T-lang Relevant to the language team, which will review and decide on the PR/issue.

Comments

@est31
Copy link
Member

est31 commented Apr 29, 2017

This is a tracking issue for the compatibility lint disallowing floating point literals in patterns.

The corresponding RFC is RFC 1445. Originally, #36890 was the only tracking issue for floats in patterns, but after it was found out that the implementation of that lint doesn't cover literals (issue #41255), another lint and tracking issue were required.

The goal of this lint is to make code like this a hard error:

fn main() {
    let x = 13.4;

    match x {
        1.0 => println!("one"),
        22.4 => println!("two"),
        3.67 => println!("three"),
        13.4 => println!("thirteen point four"),
        _ => println!("anything"),
    }
}
est31 added a commit to est31/rust that referenced this issue May 2, 2017
Adds a compatibility lint to disallow floating point literals in
patterns like in match.

See the tracking issue rust-lang#41620.
frewsxcv added a commit to frewsxcv/rust that referenced this issue May 9, 2017
…omatsakis

Implement the illegal_floating_point_literal_pattern compat lint

Adds a future-compatibility lint for the [breaking-change] introduced by issue rust-lang#41620 . cc issue rust-lang#41255 .
@SimonSapin
Copy link
Contributor

RFC 1445 is about const constants. Why are float literals restricted as well?

@SimonSapin
Copy link
Contributor

This is a breaking change (rejecting previously-accepted programs) but as far as I can tell this one is not part of an accepted RFC.

@SimonSapin
Copy link
Contributor

Is this a duplicate of #36890?

@est31
Copy link
Member Author

est31 commented May 10, 2017

RFC 1445 is about const constants. Why are float literals restricted as well?

Hmm a quick glance over the RFC seemed to confirm this. Let's discuss this in #36890, okay?

Is this a duplicate of #36890?

No, its separate. There are two separate lints. This one is about literals, the one tracked in that issue is about constants. I have opened the PR #41293 after finding out about #41255.

@withoutboats
Copy link
Contributor

RFC 1445 is about const constants. Why are float literals restricted as well?

From my perspective, primitive literals (as opposed to destructuring struct literals) are a kind of constant. The same questions raised by consts in that RFC are raised by primitive literals.

@SimonSapin
Copy link
Contributor

No, its separate. There are two separate lints. This one is about literals, the one tracked in that issue is about constants.

Yet the example given in #36890 uses literals.

From my perspective, primitive literals (as opposed to destructuring struct literals) are a kind of constant.

Given that we have a language keyword named const, I think it is reasonable to assume that constants are cont items and nothing else. If a different definition of the term is intended, it should absolutely be defined explicitly. This is not the case in this RFC.

@est31
Copy link
Member Author

est31 commented May 10, 2017

No, its separate. There are two separate lints. This one is about literals, the one tracked in that issue is about constants.

Yet the example given in #36890 uses literals.

The example quoted in the tracking issue you linked never was implemented that way. I have asked about whether to create a separate lint or add the check to the existing one, and it was decided to create a separate lint.

I guess the example should be removed from that issue's description.

@nikomatsakis
Copy link
Contributor

nikomatsakis commented May 10, 2017

I would definitely say that the RFC was intended to apply equally to all constants, whether they are literals or not. Clearly there is room for other opinions (since @SimonSapin read it differently).

Personally, I am on the fence here. I somewhat agree with @SimonSapin that the number of regressions here feels too high. Part of the reason that I agree to go with a warning was to highlight this question so that we can have a wider discussion with more of the people who are affected.

For clarity, floating point literals at present match the same way as ==. Literals cannot be NaN. Therefore, I believe the only case where == can differ from "structural equality" with a floating point value has to do with 0 vs -0, which compare as equal but are clearly structurally unequal. (Does anyone have another example? I don't claim to be an expert on floating point edge cases.)

I take the following as a given (I'm curious if others disagree):

  • We should not change the runtime behavior of matching a floating point literal (e.g., to make 0 and -0 unequal) -- silent runtime changes seem to me to be almost never desirable if they can be avoided.
  • If we support floating point non-literal constants (NLC), they should behave the same as floating point literals.

This seems to imply that if we don't make things a hard error, we can't have floating point NLC that use a purely structural match. They would always match with ==. I don't see any fundamental problem here: plausibly we could say that all user-defined types use structural equality and built-in types have their own customized behaviors. We could even tweak the behavior for NaN if we wanted, though I think that having matching behave almost like == but different (i.e., 0 == -0 but NaN == NaN) feels even more confusing to me and I'm probably opposed.

This may or may not affect plans around constant generics. It's not obvious to me that dynamic match evaluation and equality for constant generics have to be particularly related, but I can see the appeal of trying to have one notion of "structural equality" that applies equally. Maybe someone wants to make the case (@eddyb? @withoutboats?) that the two are entangled? (I'd like to hear it again.)

In other words, I see a few options:

  • Make literals illegal.
  • Continue to allow literals, but not NLC.
    • but @withoutboats objects, and quite reasonably so, to the idea that we should draw this distinction. I agree it is surprising to me that a literal and a const would behave so differently.
  • Allow both literals and floating point constants to continue with current semantics
    • We used to check for NaN and error out; we could keep doing so for now, though if we ever support generics constants of floating point type that will no longer be viable -- or at least you couldn't use those constants in a match expression.
    • Personally, I don't see the point, though I wouldn't object to warning if we ever see a NaN constant in a match arm, since it is dead-code (and I would warn about it just as we try to always warn about dead code).

I am not sure whether an RFC ought to be required, whichever path we chose, but since there seems to be disagreement, it seems plausible to open an RFC amendment to finalize the decision.

@withoutboats
Copy link
Contributor

withoutboats commented May 11, 2017

@nikomatsakis I don't think this needs to be entangled with const generics, we just need to transition to a solution for that as well as whatever we decide for match. Just renaming the attribute is probably fine.

I do feel that literals and consts should behave the same, but I don't have a strong opinion about how they ought to behave. Really whatever decision seems fine if it applies to both.

@nikomatsakis
Copy link
Contributor

I don't think this needs to be entangled with const generics, we just need to transition to a solution for that as well as whatever we decide for match.

Let me unpack this a bit to be sure I understand. You're saying:

  1. Currently, the idea for const generics equality would have been to use the same #[structural_equality] attribute to denote types that are "structurally comparable for equality".
  2. But if we want we could have distinct attributes for the match system and for const generics.

Right?

(That said, it's not entirely clear to me why const generics would even need an attribute at all; I guess it depends a lot on how we wind up defining equality. My expectation was that equality would be based more on where in the source the expression arose (i.e., we would treat constant expressions as kind of "uninterpreted functions") -- and we'd always be assuming that said functions are deterministic (because of the limits we place on const fns), and hence we consider two constant expressions "equal" if they are the same function applied to equal inputs, where this bottoms out with simple integers and other builtins. But I guess eventually we might want to extend that notion of structural equality to other kinds of expressions and types, and maybe we want some opt-in around that, unsure.)

@withoutboats
Copy link
Contributor

@nikomatsakis the issue here isn't with unevaluable const expressions but with floating point numbers and other types for which equality is not reflexive because of how that would impact unification. We don't need an attribute but it does seem that we do need const values to have a guaranteed reflexive definition of equality (which is why just using PartialEq seems problematic for consts).

@darkwater
Copy link

darkwater commented May 23, 2017

I might be missing something here, but I see this also applies to ranges, ie.

let color = match foo {
    0.0...0.1 => Color::Red,
    0.1...0.4 => Color::Yellow,
    0.4...0.8 => Color::Blue,
    _         => Color::Grey,
};

gives warning: floating-point literals cannot be used in patterns.

Is this intentional? I feel like this is a pretty common pattern.

@fschutt
Copy link
Contributor

fschutt commented May 24, 2017

Yeah, I gotta ask, how am I supposed to do clean pattern matching with floating point ranges. I know floating points are hard, but isn't there a way to make this work in ranges? Example (real) code that gives a warning now:

/// Calculates the UTM zone this longitude falls in
/// Handles exceptions for Norway / Svalbard
/// For a visual representation: https://upload.wikimedia.org/wikipedia/commons/a/a5/UTM-Zone.svg
///
/// Inputs: Longitude, in degrees
///         Latitude, in degrees
///
/// Returns: UTM Zone
///
#[allow(non_snake_case)]
pub fn get_utm_zone(lon: f64, lat: f64) -> u8 {

    let mut zone = ((lon + 180.0) / 6.0).floor() as u8 + 1;

    match lat {
        56.0..64.0 => {
            /* Zone V, Norway */
            match lon {
                3.0..6.0 => {
                    zone += 1;
                }
                _ => {}
            }
        }

        72.0..84.0 => {
            /* Zone X, Svalbard */
            match lon {
                6.0..9.0 => {
                    zone -= 1;
                }
                9.0..12.0 => {
                    zone += 1;
                }
                18.0..21.0 => {
                    zone -= 1;
                }
                21.0..24.0 => {
                    zone += 1;
                }
                30.0..33.0 => {
                    zone -= 1;
                }
                33.0..36.0 => {
                    zone += 1;
                }
                _ => {}
            }
        }

        _ => {}
    }

    zone
}

How should I write this instead? Why is it that I can do comparisons of floats in if statements, but not pattern matching in range statements? I would have to write if value < 30.0 && value > 27.0 { }, which, in the end does the same thing, but less readable.

@nikomatsakis
Copy link
Contributor

@darkwater @sharazam

I might be missing something here, but I see this also applies to ranges, ie.

Yes, there has been some back and forth on whether we ought to apply the same thing to floating points.

@vks
Copy link
Contributor

vks commented May 25, 2017

It would be very unfortunate if floating point ranges were disallowed by this. I don't see any problems with this particular pattern.

@nikomatsakis nikomatsakis added B-unstable Blocker: Implemented in the nightly compiler and unstable. T-lang Relevant to the language team, which will review and decide on the PR/issue. labels May 25, 2017
@kornelski
Copy link
Contributor

kornelski commented May 29, 2017

I was caught by this as well.

My case is:

let fudge_factor = match magic_value {
            0. ... 0.8 => 9., 
            0. ... 1.0 => 6.,
            0. ... 1.2 => 4.,
            0. ... 2.5 => 3.67, 
            _ => 3.0,
        };

To work around float comparison woes I'm using a hack of starting every range with 0. ... and depending on the specific match order instead.

I don't particularly like the current hacky/unclear/error-prone way of matching non-overlapping ranges of numbers, but I'd rather have something working until Rust gets a better replacement.

bvssvni added a commit to bvssvni/image that referenced this issue Jun 12, 2017
@donbright
Copy link

donbright commented Feb 12, 2023

(edited)... if disallowing it in match why not disallow it in == ? for that matter why does it allow the implicit conversion of base-10 decimal ascii into float at all? why not make let x = 0.3 ; an error

 let x = 0.3;
 println!("{:.40}",x);
 println!("{:?}",x==0.2999999999999999888977697537484345957637);
 println!("{:?}",x==0.3);
 println!("{:?}",x==0.2999999999999999888977697537484345957636);
 println!("{:?}",x==0.2999999999999999888977697537486);
 println!("{:?}",x==0.29999999999999998);
 println!("{:?}",x==0.2999999);

this returns

 0.2999999999999999888977697537484345957637, true, true, true, true, true, false

make it make sense. it doesn't make sense. we just live with the ambiguity and its ok for most programs. but it's problematic in match. that doesn't mean we have to totally ban floating point from match, anymore than we have to totally ban base-10 numeral literals from float literal definitions in programming languages. i think the issue is not floats but the representation of binary floats as base 10 numerals.

therefore i think a compromise could be made where some float literals should be allowed in match, like integers up to the limit of the largest int contiguously representable in the given float type, which can be exactly represented by a float. (in other words, there is some point in the float number line where integers "skip" and you can represent n but not n+1, but again you can represent n+2. so cut it off at n). for f32 this is around 16,777,217 and f64 bit its about 9,007,199,254,740,993 see so which probably covers most use cases.

matching on x = 0 for example is something you might like to use a lot when deciding whether to divide by x. and in trig calculations matching to x=1 is common and 1 is exactly representable in floating point binary.

you could also allow float literals that are not integers, if they are exactly representable in binary, which means you could allow fractions involving powers of 2. not sure of the notation.

edit maybe something like this

 match x {
    0.0..=0.5=>  // match when 0<x<=0.5  which are both exactly representable in floating point binary
    0.5+..=4.0=>  // match when 0.5<x<=4.0 which are both exactly representable in floating point binary
    4.0+..=4.5=>  // match when 4.0<x<=4.5 which are both exactly representable in floating point binary
    4.5+..=4.6 => /// ERROR, 4.6 is not exactly representable in floating point binary
    4.5..=4.75 => /// ERROR, 4.5 already included in match arm above (overlap)
    4.5+..<4.75 => // match when 4.5<x<4.75    which are both exactly representable in floating point binary
    4.75..=5 =>      // match when 4.75<= x <=5 , which are both exactly representable in floating point binary 
    _=> everything when x<0 and when x>5 including if x is NAN or infinity     
}

the compiler could check if the literals are exactly representable in floating point binary at compile time. it could also check for the "accidental holes" in the range the user is trying to describe as well, just like it currently does with ints.

@frewsxcv
Copy link
Member

I probably missed it above, but does anyone have a simple concrete example of how a match on floating point numbers would be more misleading than a conditional on the same floating point numbers?

@samuelpilz
Copy link

samuelpilz commented Aug 30, 2023

I just had this example:

I have fn calc() -> Option<f64>

I wanted to write (maybe controversial style, but I like it)

// if is calc() is out of range
if !matches!(calc(), None | Some(0.0..=1.0) { ... }

I had to write (I dont demorgan when checking for intervals)

if let Some(c) = calc() {
  if !(0 <= c && c <= 1) {...}
}

Sure, there can be some issues about exhaustiveness or gaps or reflexivity, but these well-behaved ranges are quite nice.

The inverse is even more convoluted. Am I missing something??

// calc() is missing or in range
if calc().map(|c| 0 <= c && c <= 1).unwrap_or(true) {...}

// or:
let is_ok = if let Some(c) = calc() { 0 <= c && c <= 1 } else { true };
if is_ok {...}

// float patterns: 
if matches!(calc(), None | Some(0.0..=1.0)) { ... }

@RalfJung
Copy link
Member

FWIW #84045 (which wanted to turn this into a hard error) got rejected by the lang team. So it seems like the "future compatibility" status of this lint is not accurate -- this will not be made a hard error.

We might want a lint that specifically warns about NaNs and +/- 0 in patterns because they don't behave like a bit-wise equality test, but that's quite different from what this lint currently does.

@nikomatsakis
Copy link
Contributor

Ah, good call out @RalfJung, we should probably change the status to jjust a lint?

@RalfJung
Copy link
Member

RalfJung commented Sep 16, 2023

Yes, and adjust the wording and name.

But also, why would we lint against matches!(x, 1.001) but not against x == 1.001? Their semantics is exactly the same...

@RalfJung
Copy link
Member

IMO we should just entirely remove the lint, without replacement.

@cyqsimon
Copy link
Contributor

So how do we move this forward? Being stuck in a state of limbo is worse than either outcome.

@RalfJung
Copy link
Member

There's a design meeting this week where the lang team will look at this issue and the surrounding issues related to pattern matching.

@rdrpenguin04
Copy link

What was the outcome?

@RalfJung
Copy link
Member

RalfJung commented Nov 3, 2023

Unfortunately we didn't even get to floats, we were mainly discussing consts in patterns in general. Here is something of a summary.

@NicoElbers
Copy link

Has there been any discussion since?

@RalfJung
Copy link
Member

Yes: rust-lang/rfcs#3535

matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Feb 3, 2024
make matching on NaN a hard error, and remove the rest of illegal_floating_point_literal_pattern

These arms would never be hit anyway, so the pattern makes little sense. We have had a future-compat lint against float matches in general for a *long* time, so I hope we can get away with immediately making this a hard error.

This is part of implementing rust-lang/rfcs#3535.

Closes rust-lang#41620 by removing the lint.

rust-lang/reference#1456 updates the reference to match.
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Feb 3, 2024
make matching on NaN a hard error, and remove the rest of illegal_floating_point_literal_pattern

These arms would never be hit anyway, so the pattern makes little sense. We have had a future-compat lint against float matches in general for a *long* time, so I hope we can get away with immediately making this a hard error.

This is part of implementing rust-lang/rfcs#3535.

Closes rust-lang#41620 by removing the lint.

rust-lang/reference#1456 updates the reference to match.
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Feb 5, 2024
make matching on NaN a hard error, and remove the rest of illegal_floating_point_literal_pattern

These arms would never be hit anyway, so the pattern makes little sense. We have had a future-compat lint against float matches in general for a *long* time, so I hope we can get away with immediately making this a hard error.

This is part of implementing rust-lang/rfcs#3535.

Closes rust-lang#41620 by removing the lint.

rust-lang/reference#1456 updates the reference to match.
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Feb 5, 2024
make matching on NaN a hard error, and remove the rest of illegal_floating_point_literal_pattern

These arms would never be hit anyway, so the pattern makes little sense. We have had a future-compat lint against float matches in general for a *long* time, so I hope we can get away with immediately making this a hard error.

This is part of implementing rust-lang/rfcs#3535.

Closes rust-lang#41620 by removing the lint.

rust-lang/reference#1456 updates the reference to match.
@bors bors closed this as completed in ed27148 Feb 5, 2024
rust-timer added a commit to rust-lang-ci/rust that referenced this issue Feb 5, 2024
Rollup merge of rust-lang#116284 - RalfJung:no-nan-match, r=cjgillot

make matching on NaN a hard error, and remove the rest of illegal_floating_point_literal_pattern

These arms would never be hit anyway, so the pattern makes little sense. We have had a future-compat lint against float matches in general for a *long* time, so I hope we can get away with immediately making this a hard error.

This is part of implementing rust-lang/rfcs#3535.

Closes rust-lang#41620 by removing the lint.

rust-lang/reference#1456 updates the reference to match.
flip1995 pushed a commit to flip1995/rust-clippy that referenced this issue Feb 8, 2024
make matching on NaN a hard error, and remove the rest of illegal_floating_point_literal_pattern

These arms would never be hit anyway, so the pattern makes little sense. We have had a future-compat lint against float matches in general for a *long* time, so I hope we can get away with immediately making this a hard error.

This is part of implementing rust-lang/rfcs#3535.

Closes rust-lang/rust#41620 by removing the lint.

rust-lang/reference#1456 updates the reference to match.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-floating-point Area: Floating point numbers and arithmetic A-patterns Relating to patterns and pattern matching B-unstable Blocker: Implemented in the nightly compiler and unstable. C-future-compatibility Category: Future-compatibility lints T-lang Relevant to the language team, which will review and decide on the PR/issue.
Projects
Archived in project